use std::{
    collections::{BTreeMap, HashSet},
    ffi::{OsStr, OsString},
    fs::File,
    io::{BufReader, Read},
    os::unix::{fs::MetadataExt, process::CommandExt},
    path::{Path, PathBuf},
    process::Command,
    thread::JoinHandle,
};

use argh::FromArgs;
use log::{error, info, warn};
use pipe::PipeReader;
use serde::{Deserialize, Serialize};

mod pipe;

const IOBUF_SIZE: usize = 1 * 1024 * 1024; // 1MB

#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)]
enum State {
    Start,
    Downloaded,
    Built,
    Installed,
}

#[derive(Debug, Deserialize, Serialize)]
struct PackageState {
    state: State,
    // Not empty if state is "Installed".
    version: Option<String>,
}

impl Default for PackageState {
    fn default() -> Self {
        PackageState {
            state: State::Start,
            version: None,
        }
    }
}

/// jiapkg - build package I use.
#[derive(FromArgs)]
struct Cli {
    #[argh(subcommand)]
    cmd: Cmd,
}

#[derive(FromArgs)]
#[argh(subcommand)]
enum Cmd {
    Download(CmdDownload),
    Build(CmdBuild),
    Install(CmdInstall),
    Remove(CmdRemove),
    Info(CmdInfo),
    Shell(CmdShell),
    TgzEx(CmdExTgz),
    TxzEx(CmdExTxz),
}

/// Extract tar.gz.
#[derive(FromArgs)]
#[argh(subcommand, name = "ex-txz")]
struct CmdExTxz {
    /// file to extract.
    #[argh(positional)]
    path: PathBuf,

    /// directory to output files into.
    #[argh(short = 'o', option)]
    output_dir: PathBuf,

    /// strip some components of each file in tarball.
    #[argh(option, default = "0")]
    strip_components: usize,

    /// print extracted contents.
    #[argh(short = 'v', switch)]
    verbose: bool,
}

impl CmdExTxz {
    fn txz_ex(&self) -> Result<(), ()> {
        let (th, pipe_reader) = xz_decode(&self.path)?;

        let pipe_reader = tar_unpack(
            pipe_reader,
            &self.output_dir,
            self.strip_components,
            self.verbose,
        )?;

        th.join()
            .map_err(|_| error!("failed to join thread"))?
            .map_err(|_| error!("xz decoder report error"))?;

        drop(pipe_reader);

        Ok(())
    }
}

fn xz_decode(path: &Path) -> Result<(JoinHandle<Result<(), ()>>, PipeReader), ()> {
    let reader =
        File::open(path).map_err(|err| error!("failed to open `{}': {}", path.display(), err))?;
    let mut reader = BufReader::with_capacity(IOBUF_SIZE, reader);

    let (pr, mut pw) = pipe::pipe();

    let th = std::thread::spawn(move || {
        lzma_rs::xz_decompress(&mut reader, &mut pw)
            .map_err(|err| error!("failed to decode xz: {err}"))?;
        Ok(())
    });

    Ok((th, pr))
}

/// Extract tar.gz.
#[derive(FromArgs)]
#[argh(subcommand, name = "ex-tgz")]
struct CmdExTgz {
    /// file to extract.
    #[argh(positional)]
    path: PathBuf,

    /// directory to output files into.
    #[argh(short = 'o', option)]
    output_dir: PathBuf,

    /// strip some components of each file in tarball.
    #[argh(option, default = "0")]
    strip_components: usize,

    /// print extracted contents.
    #[argh(short = 'v', switch)]
    verbose: bool,
}

impl CmdExTgz {
    fn tgz_ex(&self) -> Result<(), ()> {
        let (mut gzip_proc, reader) = gzip_decode_proc(&self.path)?;

        let reader = tar_unpack(
            reader,
            &self.output_dir,
            self.strip_components,
            self.verbose,
        )?;

        let status = gzip_proc
            .wait()
            .map_err(|err| error!("gzip decoder report error: {err}"))?;

        if !status.success() {
            error!("gzip decoder exit with non-zero: {:?}", status.code());
            return Err(());
        }

        drop(reader);

        Ok(())
    }
}

fn tar_unpack<R: Read>(
    reader: R,
    output_dir: &Path,
    strip_components: usize,
    verbose: bool,
) -> Result<R, ()> {
    let mut ar = tar::Archive::new(reader);

    for ent in ar
        .entries()
        .map_err(|err| error!("failed to list entries of tar: {err}"))?
    {
        let mut ent = ent.map_err(|err| error!("failed to read entry of tar: {err}"))?;

        let path = ent
            .path()
            .map_err(|err| error!("failed to get path of component: {err}"))?;

        let mut target_file = output_dir.to_path_buf();
        let mut strip_components = strip_components;

        for comp in path.components() {
            match comp {
                std::path::Component::Prefix(..)
                | std::path::Component::RootDir
                | std::path::Component::CurDir => {
                    warn!("skip prefix or current dir of tar content");
                    continue;
                }
                std::path::Component::ParentDir => {
                    error!(
                        "content of tar contains parent dir, which is not allowed: {}",
                        path.display()
                    );
                    return Err(());
                }
                std::path::Component::Normal(c) => {
                    if strip_components > 0 {
                        strip_components -= 1;
                    } else {
                        target_file = target_file.join(c);
                    }
                }
            }
        }

        if strip_components > 0 {
            error!(
                "failed to strip components, entry `{}' do not have that much components ({})",
                path.display(),
                strip_components
            );
            return Err(());
        }

        ensure_parent_dir(&target_file)?;

        if verbose {
            println!("{}", path.display());
        }

        let out_of_date = is_target_out_of_date(&target_file, &ent);

        if out_of_date {
            ent.unpack(target_file)
                .map_err(|err| error!("unpack error: {}", err))?;
        }
    }

    Ok(ar.into_inner())
}

fn is_target_out_of_date<R: Read>(target_file: &Path, ent: &tar::Entry<'_, R>) -> bool {
    let Ok(meta) = std::fs::metadata(target_file) else {
        return true;
    };

    let target_mtime = meta.mtime();

    let Ok(ent_mtime) = ent.header().mtime() else {
        return true;
    };

    ent_mtime as i64 > target_mtime
}

fn gzip_decode_proc(path: &Path) -> Result<(std::process::Child, std::process::ChildStdout), ()> {
    let fs_reader =
        File::open(path).map_err(|err| error!("failed to open `{}': {}", path.display(), err))?;

    let mut cmd = Command::new("gzip")
        .arg("-d")
        .arg("-c")
        .stdin(fs_reader)
        .stdout(std::process::Stdio::piped())
        .spawn()
        .map_err(|err| error!("failed to start gzip: {err}"))?;

    let stdout = cmd.stdout.take().unwrap();

    Ok((cmd, stdout))
}

/// Start a new shell with packages used.
#[derive(FromArgs)]
#[argh(subcommand, name = "shell")]
struct CmdShell {
    /// packages to use.
    #[argh(short = 'p', option)]
    packages: Vec<String>,

    /// command arguments to run. Run shell used by jiapkg if empty.
    #[argh(positional)]
    cmd: Vec<String>,
}

impl CmdShell {
    fn shell(&self, env: &Env) -> Result<(), ()> {
        let mut q = Vec::new();

        for each in &self.packages {
            q.push(each.to_owned());
        }

        let pkgs = find_pkg_dep_tree(q, env)?;

        let mut cmd = match self.cmd.as_slice() {
            [] => Command::new(&env.sh_exe),
            [cmd, args @ ..] => {
                let mut cmd = Command::new(cmd);
                cmd.args(args);
                cmd
            }
        };

        let err = cmd
            .env(
                "PATH",
                join_path(
                    pkgs.iter()
                        .map(|pkg| env.install_dir.join(pkg).join("bin"))
                        .chain(env.default_path.iter().cloned()),
                ),
            )
            .env(
                "JIAPKG_EXE",
                std::env::current_exe()
                    .map_err(|err| error!("failed to find current exe: {err}"))?,
            )
            .env("JIAPKG_SHELL", "y")
            .exec();

        error!("failed to start shell: {err}");

        return Err(());
    }
}

fn find_pkg_dep_tree(mut q: Vec<String>, env: &Env) -> Result<HashSet<String>, ()> {
    let mut pkgs = HashSet::new();

    while let Some(pkg) = q.pop() {
        if !pkgs.insert(pkg.clone()) {
            continue;
        }

        let pkg = env.load_assets(&pkg)?;

        for dep in pkg.package.deps {
            q.push(dep.to_owned());
        }
    }

    Ok(pkgs)
}

/// Remove a package.
#[derive(FromArgs)]
#[argh(subcommand, name = "remove")]
struct CmdRemove {
    /// name of package.
    #[argh(positional)]
    package: String,
}

impl CmdRemove {
    fn remove(&self, env: &Env) -> Result<(), ()> {
        let mut pkg = env.load_pkg_state(&self.package)?;

        if pkg.state != State::Installed {
            error!("package `{}' is not installed", self.package);
            pkg.state = State::Start;
            env.dump_pkg_info(&self.package, &pkg)?;
            return Err(());
        }

        ensure_cmd(
            Command::new("rm")
                .arg("-r")
                .arg(env.install_dir.join(&self.package)),
        )?;

        pkg.state = State::Start;
        env.dump_pkg_info(&self.package, &pkg)?;

        Ok(())
    }
}

/// Show info of a package.
#[derive(FromArgs)]
#[argh(subcommand, name = "info")]
struct CmdInfo {
    /// name of package.
    #[argh(positional)]
    package: String,

    /// show install directory.
    #[argh(switch)]
    install_dir: bool,
}

impl CmdInfo {
    fn info(&self, env: &Env) -> Result<(), ()> {
        if self.install_dir {
            println!("{}", env.install_dir.join(&self.package).display());
        }

        Ok(())
    }
}

/// Install a package.
#[derive(FromArgs)]
#[argh(subcommand, name = "install")]
struct CmdInstall {
    /// name of package.
    #[argh(positional)]
    package: String,

    /// remove before install.
    #[argh(switch)]
    reinstall: bool,
}

impl CmdInstall {
    fn install(&self, env: &Env) -> Result<(), ()> {
        println!("install {}", self.package);

        let assets = env.load_assets(&self.package)?;

        for package in assets.package.deps {
            CmdInstall {
                package,
                reinstall: false,
            }
            .install(env)?;
        }

        CmdBuild {
            package: self.package.clone(),
            rebuild: false,
        }
        .build(env)?;

        let mut pkg_info = env.load_pkg_state(&self.package)?;

        if !self.reinstall && pkg_info.state >= State::Installed {
            info!(
                "skip install {} because of current state is: {:?}",
                self.package, pkg_info.state,
            );
            return Ok(());
        }

        let build_dir = env.build_dir(&self.package);

        let mut cmd = Command::new(&env.sh_exe);

        cmd.arg("-eu");

        ensure_cmd(
            package_script_env(
                cmd.arg(env.package_script(&self.package)).arg("install"),
                &self.package,
                env,
            )?
            .current_dir(&build_dir),
        )?;

        // pkg_info.version = assets.asset.values().map(|x| x.version)
        pkg_info.state = State::Installed;
        // pkg_info.version = Some();
        env.dump_pkg_info(&self.package, &pkg_info)?;

        Ok(())
    }
}

/// Download assets of a package.
#[derive(FromArgs)]
#[argh(subcommand, name = "download")]
struct CmdDownload {
    /// name of package.
    #[argh(positional)]
    package: String,
}

impl CmdDownload {
    fn download(&self, env: &Env) -> Result<(), ()> {
        let mut pkg_info = env.load_pkg_state(&self.package)?;

        if pkg_info.state >= State::Downloaded {
            info!(
                "skip download {} because of current state is: {:?}",
                self.package, pkg_info.state
            );
            return Ok(());
        }

        let mut assets = env.load_assets(&self.package)?;

        for (name, asset) in assets.asset.iter_mut() {
            asset.download(&self.package, name, env)?;
        }

        pkg_info.state = State::Downloaded;

        env.dump_pkg_info(&self.package, &pkg_info)?;

        Ok(())
    }
}

/// Build a package.
#[derive(FromArgs)]
#[argh(subcommand, name = "build")]
struct CmdBuild {
    /// name of package.
    #[argh(positional)]
    package: String,

    /// still build again if current package is built.
    #[argh(switch)]
    rebuild: bool,
}

fn join_path<I, S>(iter: I) -> OsString
where
    I: Iterator<Item = S>,
    S: AsRef<OsStr>,
{
    let mut string = OsString::new();

    for each in iter {
        if !string.is_empty() {
            string.push(":");
        }

        string.push(each.as_ref());
    }

    string
}

// Asset with this name is extract to build directory instead of a subdirectory.
const MAIN_ASSET: &'static str = "main";

impl CmdBuild {
    fn build(&self, env: &Env) -> Result<(), ()> {
        let assets = env.load_assets(&self.package)?;

        // Make sure there is a main asset.
        assets.find_main_assets()?;

        for package in assets.package.deps {
            CmdInstall {
                package,
                reinstall: false,
            }
            .install(env)?;
        }

        CmdDownload {
            package: self.package.clone(),
        }
        .download(env)?;

        let mut pkg_info = env.load_pkg_state(&self.package)?;

        if !self.rebuild && pkg_info.state >= State::Built {
            info!(
                "skip build {} because of current state is: {:?}",
                self.package, pkg_info.state
            );
            return Ok(());
        }

        let assets = env.load_assets(&self.package)?;

        let download_dir = env.download_dir(&self.package);
        let build_dir = env.build_dir(&self.package);

        ensure_clean_dir(&build_dir);

        for (asset_name, asset) in &assets.asset {
            match &asset.data {
                AssetData::Git(_) => {
                    let dst = asset_extract_dir(&build_dir, &asset_name);

                    let _ = std::fs::create_dir_all(&dst);
                    CmdExTgz {
                        path: download_dir.join(format!("{}-{}.tar.gz", asset_name, asset.version)),
                        output_dir: dst,
                        strip_components: 0,
                        verbose: false,
                    }
                    .tgz_ex()?;
                }
                AssetData::Tgz(tarball) => {
                    let dst = asset_extract_dir(&build_dir, &asset_name);

                    let _ = std::fs::create_dir_all(&dst);
                    CmdExTgz {
                        path: download_dir.join(format!("{}-{}.tar.gz", asset_name, asset.version)),
                        output_dir: dst,
                        strip_components: tarball.strip_components.unwrap_or(0),
                        verbose: false,
                    }
                    .tgz_ex()?;
                }
                AssetData::Txz(tarball) => {
                    let dst = asset_extract_dir(&build_dir, &asset_name);

                    let _ = std::fs::create_dir_all(&dst);
                    CmdExTxz {
                        path: download_dir.join(format!("{}-{}.tar.xz", asset_name, asset.version)),
                        output_dir: dst,
                        strip_components: tarball.strip_components.unwrap_or(0),
                        verbose: false,
                    }
                    .txz_ex()?;
                }
            }
        }

        ensure_cmd(
            package_script_env(
                Command::new(&env.sh_exe)
                    .arg("-eu")
                    .arg(env.package_script(&self.package))
                    .arg("build"),
                &self.package,
                env,
            )?
            .current_dir(&build_dir),
        )?;

        pkg_info.state = State::Built;
        env.dump_pkg_info(&self.package, &pkg_info)?;

        Ok(())
    }
}

fn asset_extract_dir(build_dir: &PathBuf, asset_name: &str) -> PathBuf {
    if asset_name == MAIN_ASSET {
        build_dir.to_owned()
    } else {
        build_dir.join(asset_name)
    }
}

// Set envs for "package" script of a package.
fn package_script_env<'c>(
    cmd: &'c mut Command,
    pkg: &str,
    env: &Env,
) -> Result<&'c mut Command, ()> {
    let deps = find_pkg_dep_tree(vec![pkg.to_string()], env)?;

    info!("deps of {}: {:?}", pkg, deps);

    Ok(cmd
        .env(
            "PATH",
            join_path(
                deps.iter()
                    .map(|dep| env.install_dir.join(dep).join("bin"))
                    .chain(env.default_path.iter().cloned()),
            ),
        )
        .env(
            "JIAPKG_EXE",
            std::env::current_exe()
                .map_err(|err| error!("failed to get current exe path: {err}"))?,
        )
        .env("JIAPKG_INSTALL_DIR", env.install_dir.join(pkg))
        .env("JIAPKG_JOBS", env.build_jobs.to_string()))
}

fn ensure_clean_dir(build_dir: &Path) {
    if build_dir.is_dir() {
        let _ = std::fs::remove_dir_all(build_dir);
    }

    let _ = std::fs::create_dir_all(build_dir);
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct AssetsFile {
    #[serde(default = "Default::default")]
    package: AssetsFilePackage,

    #[serde(default = "Default::default")]
    asset: BTreeMap<String, Asset>,
}

impl AssetsFile {
    fn find_main_assets(&self) -> Result<&Asset, ()> {
        for (name, asset) in self.asset.iter() {
            if name == MAIN_ASSET {
                return Ok(asset);
            }
        }

        error!("can't find asset named 'main'");
        Err(())
    }
}

#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct AssetsFilePackage {
    #[serde(default = "Default::default")]
    deps: Vec<String>,
}

// FIXME: deny_unknown_fields here cause error on parse. "type" is not known.
#[derive(Debug, Serialize, Deserialize)]
// #[serde(deny_unknown_fields)]
struct Asset {
    version: String,
    #[serde(flatten)]
    data: AssetData,
}

impl Asset {
    fn download(&self, pkg: &str, asset_name: &str, env: &Env) -> Result<(), ()> {
        match &self.data {
            AssetData::Tgz(tgz) => {
                let dst = env
                    .download_dir(pkg)
                    .join(format!("{}-{}.tar.gz", asset_name, self.version));

                if dst.is_file() {
                    return Ok(());
                }

                ensure_parent_dir(&dst)?;

                ensure_cmd(wget_cmd().arg("-O").arg(dst).arg(&tgz.url))?;
            }
            AssetData::Txz(tgz) => {
                let dst = env
                    .download_dir(pkg)
                    .join(format!("{}-{}.tar.xz", asset_name, self.version));

                if dst.is_file() {
                    return Ok(());
                }

                ensure_parent_dir(&dst)?;

                ensure_cmd(wget_cmd().arg("-O").arg(dst).arg(&tgz.url))?;
            }
            AssetData::Git(git) => {
                let dst = env
                    .download_dir(pkg)
                    .join(format!("{}-{}", asset_name, self.version));

                if dst.is_dir() {
                    return Ok(());
                }

                ensure_parent_dir(&dst)?;

                ensure_cmd(
                    Command::new("git")
                        .arg("clone")
                        .arg(&git.url)
                        .arg(&dst)
                        .arg("--depth=1")
                        .arg("-b")
                        .arg(git.checkout.as_ref().unwrap_or(&self.version))
                        .env("GIT_CONFIG_COUNT", "1")
                        .env("GIT_CONFIG_KEY_0", "advice.detachedHead")
                        .env("GIT_CONFIG_VALUE_0", "false"),
                )?;

                let dst_tgz = format!("../{}-{}.tar.gz", asset_name, self.version);

                ensure_cmd(
                    Command::new("git")
                        .arg("archive")
                        .arg("HEAD")
                        .arg("-o")
                        .arg(&dst_tgz)
                        .current_dir(&dst),
                )?
            }
        }

        Ok(())
    }
}

fn wget_cmd() -> Command {
    let mut cmd = Command::new("wget");
    cmd.arg("-U").arg("Mozilla");
    cmd
}

fn ensure_parent_dir(dst: &Path) -> Result<(), ()> {
    if let Some(parent) = dst.parent() {
        std::fs::create_dir_all(parent)
            .map_err(|err| error!("failed to create parent dir: {err}"))?;
    }

    Ok(())
}

/*
fn ensure_cmd_output(cmd: &mut Command) -> Result<Vec<u8>, ()> {
    cmd.stdout(std::process::Stdio::piped());
    cmd.stderr(std::process::Stdio::inherit());

    info!("> {:?}", cmd);

    let output = cmd
        .output()
        .map_err(|err| error!("failed to start `{:?}': {err}", cmd.get_program()))?;

    if !output.status.success() {
        error!("`{:?}' report error", cmd.get_program());
        return Err(());
    }

    Ok(output.stdout)
}
*/

fn ensure_cmd(cmd: &mut Command) -> Result<(), ()> {
    info!("> {:?}", cmd);

    let status = cmd
        .status()
        .map_err(|err| error!("failed to start `{:?}': {err}", cmd.get_program()))?;

    if !status.success() {
        error!("`{:?}' report error", cmd.get_program());
        return Err(());
    }

    Ok(())
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields, tag = "type")]
enum AssetData {
    Git(AssetGit),
    Tgz(AssetTarball),
    Txz(AssetTarball),
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct AssetGit {
    url: String,
    // Default be version of asset.
    checkout: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct AssetTarball {
    url: String,
    strip_components: Option<usize>,
}

#[derive(Debug)]
struct Env {
    // Contains package infomations, assets and scripts.
    pkg_dir: PathBuf,

    // Cache directories.
    cache_dir: PathBuf,

    // Install packages to this root.
    install_dir: PathBuf,

    // Suggestion build jobs.
    build_jobs: usize,

    // Default list of PATH.
    default_path: Vec<PathBuf>,

    // Path to a posix shell that jiapkg use.
    sh_exe: PathBuf,
}

impl Env {
    fn new() -> Result<Self, ()> {
        let jiapkg_dir = dirs::home_dir()
            .ok_or_else(|| error!("can't find home dir"))?
            .join("jiapkg");

        Ok(Self {
            pkg_dir: std::env::var_os("JIAPKG_PKG_DIR")
                .unwrap_or(jiapkg_dir.join("jiapkg-pkg").into())
                .into(),
            cache_dir: jiapkg_dir.join("cache").to_path_buf(),
            install_dir: jiapkg_dir.join("install").to_path_buf(),
            build_jobs: num_cpus::get_physical(),
            default_path: vec![
                Path::new("/bin").to_path_buf(),
                Path::new("/usr/bin").to_path_buf(),
            ],
            sh_exe: Path::new("/bin/sh").to_path_buf(),
        })
    }

    fn package_script(&self, pkg: &str) -> PathBuf {
        self.pkg_dir.join(pkg).join("package")
    }

    fn dump_pkg_info(&self, pkg: &str, info: &PackageState) -> Result<(), ()> {
        let path = self.pkg_state_path(pkg);

        ensure_parent_dir(&path)?;

        let info = toml::to_string_pretty(info)
            .map_err(|err| error!("failed to serialize package info: {err}"))?;

        std::fs::write(&path, &info)
            .map_err(|err| error!("failed to write package info to {path:?}: {err}"))?;

        Ok(())
    }

    fn load_pkg_state(&self, pkg: &str) -> Result<PackageState, ()> {
        let path = self.pkg_state_path(pkg);

        if !path.is_file() {
            return Ok(PackageState::default());
        }

        let content = std::fs::read_to_string(&path)
            .map_err(|err| error!("failed to open package info file for {pkg}: {err}"))?;

        let info = toml::from_str(&content)
            .map_err(|err| error!("failed to parse package info for {pkg}: {err}"))?;

        Ok(info)
    }

    fn pkg_state_path(&self, pkg: &str) -> PathBuf {
        self.install_dir
            .join("_")
            .join("package_state")
            .join(format!("{pkg}.toml"))
    }

    fn load_assets(&self, pkg: &str) -> Result<AssetsFile, ()> {
        let path = self.pkg_dir.join(pkg).join("assets.toml");

        let content = std::fs::read_to_string(&path)
            .map_err(|err| error!("failed to open assets file for {pkg}: {err}"))?;

        let assets = toml::from_str(&content)
            .map_err(|err| error!("failed to parse assets file for {pkg}: {err}"))?;

        Ok(assets)
    }

    fn download_dir(&self, pkg: &str) -> PathBuf {
        self.cache_dir.join("download").join(pkg)
    }

    fn build_dir(&self, pkg: &str) -> PathBuf {
        self.cache_dir.join("build").join(pkg)
    }
}

fn main() {
    if try_main().is_err() {
        std::process::exit(1);
    }
}

fn try_main() -> Result<(), ()> {
    env_logger::builder()
        .filter_level(log::LevelFilter::Warn)
        .parse_env("JIAPKG_LOG")
        .try_init()
        .map_err(|err| error!("failed to init logger: {err}"))?;

    let cli: Cli = argh::from_env();
    let env = Env::new()?;

    match cli.cmd {
        Cmd::Download(x) => x
            .download(&env)
            .map_err(|_| error!("failed to download: {}", x.package)),
        Cmd::Build(x) => x
            .build(&env)
            .map_err(|_| error!("failed to build: {}", x.package)),
        Cmd::Install(x) => x
            .install(&env)
            .map_err(|_| error!("failed to install: {}", x.package)),
        Cmd::Info(x) => x
            .info(&env)
            .map_err(|_| error!("failed to get info of: {}", x.package)),
        Cmd::Remove(x) => x
            .remove(&env)
            .map_err(|_| error!("failed remove package: {}", x.package)),
        Cmd::TgzEx(x) => x.tgz_ex(),
        Cmd::TxzEx(x) => x.txz_ex(),
        Cmd::Shell(x) => x.shell(&env),
    }
}
